#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import parent
from testlib import *

@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg")
class TestSuperuser(MachineCase):

    def testBasic(self):
        b = self.browser

        # Log in with all defaults
        self.login_and_go()
        b.wait_text("#super-user-indicator", "Administrative access")

        # Drop privileges
        b.click("#super-user-indicator button")
        b.click(".modal-dialog:contains('Switch to limited access') button:contains('Limit access')")
        b.wait_not_present(".modal-dialog:contains('Switch to limited access')")
        b.wait_text("#super-user-indicator", "Limited access")

        # Check they are still gone after logout
        b.relogin()
        b.leave_page()
        b.wait_text("#super-user-indicator", "Limited access")

        # Get them back
        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "foobar")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_text("#super-user-indicator", "Administrative access")

        # Check we still have them after logout
        b.relogin()
        b.leave_page()
        b.wait_text("#super-user-indicator", "Administrative access")

    def testNoPasswd(self):
        b = self.browser
        m = self.machine

        # Log in with limited access
        self.login_and_go(superuser=False)
        b.wait_text("#super-user-indicator", "Limited access")

        # Give us password-less sudo and use it
        m.write("/etc/sudoers.d/admin", "admin ALL=(ALL) NOPASSWD:ALL")
        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "You now have administrative access")
        b.click(".modal-dialog:contains('Administrative access') button:contains('Close')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_text("#super-user-indicator", "Administrative access")

    def testTwoPasswds(self):
        b = self.browser
        m = self.machine

        # Log in with limited access
        self.login_and_go(superuser=False)
        b.wait_text("#super-user-indicator", "Limited access")

        # Configure the sudo PAM stack to make two prompts
        if "debian" in m.image or "ubuntu" in m.image:
            m.write("/etc/pam.d/sudo", """
auth required pam_unix.so
auth required /usr/lib/cockpit-test-assets/mock-pam-conv-mod.so
@include common-account
@include common-session-noninteractive
""")
        else:
            m.write("/etc/pam.d/sudo", """
auth required pam_unix.so
auth required /usr/lib/cockpit-test-assets/mock-pam-conv-mod.so
account include system-auth
password include system-auth
session include system-auth
""")

        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "foobar")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "universe and everything")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "42")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_text("#super-user-indicator", "Administrative access")

    def testWrongPasswd(self):
        b = self.browser

        self.allow_authorize_journal_messages()

        # Log in with limited access
        self.login_and_go(superuser=False)
        b.wait_text("#super-user-indicator", "Limited access")

        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "wrong")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "wronger")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "wrongest")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "This didn't work")
        b.click(".modal-dialog:contains('Administrative access') button:contains('Cancel')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_text("#super-user-indicator", "Limited access")

    def testNotAdmin(self):
        b = self.browser
        m = self.machine

        # Remove special treatment of the "admin" group on Ubuntu.
        # Our main test user is unfortunately called "admin" and has
        # "admin" as its primary group.
        #
        if "ubuntu" in m.image:
            m.execute("sed -i -e '/^%admin/d' /etc/sudoers")

        m.execute("gpasswd -d admin %s" % m.get_admin_group())
        self.allow_journal_messages("admin is not in the sudoers file.  This incident will be reported.")

        self.login_and_go()
        b.wait_text("#super-user-indicator", "Limited access")

        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "You can not gain administrative access")
        b.click(".modal-dialog:contains('Administrative access') button:contains('Close')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")

    def testBrokenBridgeConfig(self):
        b = self.browser
        m = self.machine

        m.needs_writable_usr()
        m.write("/usr/share/cockpit/shell/override.json", """
{
  "bridges": [
    {
      "privileged": true,
      "spawn": [
        "sudo",
        "-A",
        "cockpit-bridge",
        "--privileged"
      ]
    }
  ]
}
""")

        self.allow_journal_messages("sudo: no askpass program specified, try setting SUDO_ASKPASS")

        self.login_and_go(superuser=False)
        b.wait_text("#super-user-indicator", "Limited access")

        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Something went wrong")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "internal-error")
        b.click(".modal-dialog:contains('Administrative access') button:contains('Close')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_text("#super-user-indicator", "Limited access")

    def testOverview(self):
        b = self.browser

        self.login_and_go("/system", superuser=False)
        b.wait_visible(".pf-c-alert:contains('Web console is running in limited access mode.')")
        b.click(".pf-c-alert:contains('Web console is running in limited access mode.') button:contains('Turn on')")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "foobar")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_not_visible(".pf-c-alert:contains('Web console is running in limited access mode.')")

@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg")
class TestSuperuserOldShell(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20" },
        "machine2": {"address": "10.111.113.2/20", "image": "centos-7" },
    }

    def test(self):
        b = self.browser
        m = self.machine

        m.start_cockpit()

        # Use m1 to login into m2
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.click("#show-other-login-options")
        b.wait_visible("#server-group")
        b.set_val("#server-field", "10.111.113.2")
        b.click('#login-button')
        b.wait_in_text("#server-name", "10.111.113.2")
        b.wait_visible("#conversation-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
        b.click('#login-button')
        b.expect_load()
        b.wait_present('#content')
        b.wait_visible('#content')

        # The old shell should have gotten the password from cockpit-ws and it should work
        b.enter_page("/system")
        b.click("#shutdown-group > button:contains('Restart')")
        b.wait_popup("shutdown-dialog")
        b.click("#shutdown-dialog button:contains('Restart')")
        b.wait_popdown("shutdown-dialog")

@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg")
class TestSuperuserOldWebserver(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20", "image": "centos-7" },
        "machine2": {"address": "10.111.113.2/20" },
    }

    def test(self):
        b = self.browser
        m = self.machine

        m.execute("firewall-cmd --add-service cockpit")
        m.start_cockpit()

        # Use m1 to login into m2
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked('#authorized-input', True)
        b.click("#show-other-login-options")
        b.wait_visible("#server-group")
        b.set_val("#server-field", "10.111.113.2")
        b.click('#login-button')
        b.wait_in_text("#server-name", "10.111.113.2")
        b.wait_visible("#conversation-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
        b.click('#login-button')
        b.expect_load()
        b.wait_present('#content')
        b.wait_visible('#content')

        # We should have gotten the password from the old cockpit-ws and it should work
        b.wait_text("#super-user-indicator", "Administrative access")
        b.go("/playground/test")
        b.enter_page("/playground/test")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

    def testNotAuth(self):
        b = self.browser
        m = self.machine

        m.execute("firewall-cmd --add-service cockpit")
        m.start_cockpit()

        # Use m1 to login into m2, but don't reuse the password
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked('#authorized-input', False)
        b.click("#show-other-login-options")
        b.wait_visible("#server-group")
        b.set_val("#server-field", "10.111.113.2")
        b.click('#login-button')
        b.wait_in_text("#server-name", "10.111.113.2")
        b.wait_visible("#conversation-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
        b.click('#login-button')
        b.expect_load()
        b.wait_present('#content')
        b.wait_visible('#content')

        # We should not have gotten the password from the old
        # cockpit-ws, but we can get it back.

        b.wait_text("#super-user-indicator", "Limited access")
        b.go("/playground/test")
        b.enter_page("/playground/test")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'access-denied')

        b.switch_to_top()
        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "foobar")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_text("#super-user-indicator", "Administrative access")
        b.go("/playground/test")
        b.enter_page("/playground/test")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg")
class TestSuperuserDashboard(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20" },
        "machine2": {"address": "10.111.113.2/20" },
    }

    def test(self):
        b = self.browser

        self.login_and_go()
        b.go("/@10.111.113.2")
        b.wait_visible("#machine-troubleshoot")
        b.click('#machine-troubleshoot')
        b.wait_popup('troubleshoot-dialog')
        b.wait_visible('#troubleshoot-dialog button:contains("Add")')
        b.click('#troubleshoot-dialog button:contains("Add")')
        b.wait_in_text('#troubleshoot-dialog', "Fingerprint")
        b.click('#troubleshoot-dialog button:contains("Connect")')
        b.wait_in_text('#troubleshoot-dialog', "Unable to log in")
        b.set_val("#login-custom-password", "foobar")
        b.click('#troubleshoot-dialog button:contains("Log in")')
        b.wait_popdown('troubleshoot-dialog')

        # There should be no superuser indicator in the Overview

        b.go("/@10.111.113.2/system")
        b.enter_page("/system", host="10.111.113.2")
        b.wait_not_present(".ct-overview-header-actions button:contains('Administrative access')")
        b.wait_not_present(".ct-overview-header-actions button:contains('Limited access')")
        b.leave_page()

        # The superuser indicator in the Shell should apply to machine2

        b.wait_text("#super-user-indicator", "Administrative access")
        b.go("/@10.111.113.2/playground/test")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

        b.leave_page()
        b.click("#super-user-indicator button")
        b.click(".modal-dialog:contains('Switch to limited access') button:contains('Limit access')")
        b.wait_not_present(".modal-dialog:contains('Switch to limited access')")
        b.wait_text("#super-user-indicator", "Limited access")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'access-denied')

        b.leave_page()
        b.click("#super-user-indicator button")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "foobar")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_text("#super-user-indicator", "Administrative access")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg")
class TestSuperuserOldDashboard(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20", "image": "centos-7" },
        "machine2": {"address": "10.111.113.2/20" },
    }

    def test(self):
        b = self.browser
        m = self.machines["machine1"]

        m.execute("firewall-cmd --add-service cockpit")
        m.start_cockpit()

        # Log into m1 and add m2 to the dashboard
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked('#authorized-input', True)
        b.click('#login-button')
        b.expect_load()
        b.wait_present('#content')
        b.wait_visible('#content')

        b.go("/@10.111.113.2")
        b.wait_visible("#machine-troubleshoot")
        b.click('#machine-troubleshoot')
        b.wait_popup('troubleshoot-dialog')
        b.click('#troubleshoot-dialog button:contains("Add")')
        b.wait_in_text('#troubleshoot-dialog', "Fingerprint")
        b.click('#troubleshoot-dialog button:contains("Connect")')
        b.wait_popdown('troubleshoot-dialog')

        # There should be a superuser button in the Overview of machine2

        b.enter_page("/system", host="10.111.113.2")
        b.click(".ct-overview-header-actions button:contains('Administrative access')")
        b.click(".modal-dialog:contains('Switch to limited access') button:contains('Limit access')")
        b.wait_not_present(".modal-dialog:contains('Switch to limited access')")
        b.wait_present(".ct-overview-header-actions button:contains('Limited access')")
        b.go("/@10.111.113.2/playground/test")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'access-denied')

        b.go("/@10.111.113.2")
        b.enter_page("/system", host="10.111.113.2")
        b.click(".ct-overview-header-actions button:contains('Limited access')")
        b.wait_in_text(".modal-dialog:contains('Administrative access')", "Please authenticate")
        b.set_input_text(".modal-dialog:contains('Administrative access') input", "foobar")
        b.click(".modal-dialog button:contains('Authenticate')")
        b.wait_not_present(".modal-dialog:contains('Administrative access')")
        b.wait_present(".ct-overview-header-actions button:contains('Administrative access')")
        b.go("/@10.111.113.2/playground/test")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg")
class TestSuperuserDashboardOldMachine(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20" },
        "machine2": {"address": "10.111.113.2/20", "image": "centos-7" },
    }

    def test(self):
        b = self.browser

        self.login_and_go()
        b.go("/@10.111.113.2")
        b.wait_visible("#machine-troubleshoot")
        b.click('#machine-troubleshoot')
        b.wait_popup('troubleshoot-dialog')
        b.click('#troubleshoot-dialog button:contains("Add")')
        b.wait_in_text('#troubleshoot-dialog', "Fingerprint")
        b.click('#troubleshoot-dialog button:contains("Connect")')
        b.wait_in_text('#troubleshoot-dialog', "Unable to log in")
        b.set_val("#login-custom-password", "foobar")
        b.click('#troubleshoot-dialog button:contains("Log in")')
        b.wait_popdown('troubleshoot-dialog')

        # Since user and password are the same on machine2, we should
        # have gotten admin rights.

        b.enter_page("/system", host="10.111.113.2")
        b.click("#shutdown-group > button:contains('Restart')")
        b.wait_popup("shutdown-dialog")
        b.click("#shutdown-dialog button:contains('Restart')")
        b.wait_popdown("shutdown-dialog")

if __name__ == '__main__':
    test_main()
